﻿using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Reflection;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace DataLayer.Generic
{
    public class GenericObjectDataSourceView : ObjectDataSourceView
    {
        const BindingFlags NeedlesslyPrivate = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
        const BindingFlags NeedlesslyPrivateStatic = BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly;

        static readonly FieldInfo s_Owner;
        static readonly MethodInfo s_GetType;
        static readonly MethodInfo s_TryGetDataObjectType;
        static readonly MethodInfo s_MergeDictionaries;
        static readonly MethodInfo s_GetResolvedMethodData;
        static readonly MethodInfo s_InvokeMethod;
        static readonly MethodInfo s_BuildObjectValue;
        static readonly FieldInfo s_AffectedRows;
        static readonly FieldInfo s_Parameters;

        static GenericObjectDataSourceView()
        {
            s_Owner = typeof(ObjectDataSourceView).GetField("_owner", NeedlesslyPrivate);

            Type[] getType = new Type[] { typeof(string) };
            s_GetType = typeof(ObjectDataSourceView).GetMethod("GetType", NeedlesslyPrivate, null, getType, null);

            Type[] tryGetDataObjectType = new Type[] { };
            s_TryGetDataObjectType = typeof(ObjectDataSourceView).GetMethod("TryGetDataObjectType", NeedlesslyPrivate, null, tryGetDataObjectType, null);

            Type[] mergeDictionaries = new Type[] { typeof(ParameterCollection), typeof(System.Collections.IDictionary), typeof(IDictionary) };
            s_MergeDictionaries = typeof(ObjectDataSourceView).GetMethod("MergeDictionaries", NeedlesslyPrivateStatic, null, mergeDictionaries, null);

            Type[] getResolvedMethodData = new Type[] { typeof(Type), typeof(string), typeof(Type), typeof(object), typeof(object), typeof(DataSourceOperation) };
            s_GetResolvedMethodData = typeof(ObjectDataSourceView).GetMethod("GetResolvedMethodData", NeedlesslyPrivate, null, getResolvedMethodData, null);

            Type[] buildObjectValue = new Type[] { typeof(object), typeof(Type), typeof(string) };
            s_BuildObjectValue = typeof(ObjectDataSourceView).GetMethod("BuildObjectValue", NeedlesslyPrivateStatic, null, buildObjectValue, null);

            Type objectDataSourceMethod = typeof(ObjectDataSourceView).GetNestedType("ObjectDataSourceMethod", BindingFlags.NonPublic);
            s_Parameters = objectDataSourceMethod.GetField("Parameters", NeedlesslyPrivate);

            Type[] invokeMethod = new Type[] { objectDataSourceMethod };
            s_InvokeMethod = typeof(ObjectDataSourceView).GetMethod("InvokeMethod", NeedlesslyPrivate, null, invokeMethod, null);

            Type objectDataSourceResult = typeof(ObjectDataSourceView).GetNestedType("ObjectDataSourceResult", BindingFlags.NonPublic);
            s_AffectedRows = objectDataSourceResult.GetField("AffectedRows", NeedlesslyPrivate);
        }

        public GenericObjectDataSourceView(ObjectDataSource owner, string name, HttpContext context)
            : base(owner, name, context)
        {
        }

        protected override int ExecuteDelete(IDictionary keys, IDictionary oldValues)
        {
            if (!this.CanDelete)
            {
                throw new NotSupportedException(ExposedSR.GetString(ExposedSR.DeleteNotSupported, this.Owner.ID));
            }

            // we only change the behavior of sources that provide a DataObjectTypeName
            if (String.IsNullOrEmpty(this.DataObjectTypeName))
                return base.ExecuteDelete(keys, oldValues);

            Type sourceType = this.GetType(this.TypeName);
            Type dataObjectType = this.TryGetDataObjectType();
            IDictionary deleteParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);

            if (this.ConflictDetection == ConflictOptions.CompareAllValues)
            {
                if (oldValues == null || oldValues.Count == 0)
                {
                    throw new InvalidOperationException(ExposedSR.GetString(ExposedSR.Pessimistic, ExposedSR.GetString(ExposedSR.Delete), this.Owner.ID, "oldValues"));
                }

                MergeDictionaries(this.DeleteParameters, oldValues, deleteParameters);
            }

            MergeDictionaries(this.DeleteParameters, keys, deleteParameters);
            object dataObject = this.BuildDataObject(dataObjectType, deleteParameters, DataSourceOperation.Delete);

            object deleteMethod = this.GetResolvedMethodData(sourceType, this.DeleteMethod, dataObjectType, dataObject, null, DataSourceOperation.Delete);
            IOrderedDictionary parameters = ExtractMethodParameters(deleteMethod);
            ObjectDataSourceMethodEventArgs args = new ObjectDataSourceMethodEventArgs(parameters);
            this.OnDeleting(args);

            if (args.Cancel)
            {
                return 0;
            }

            object result = this.InvokeMethod(deleteMethod);

            this.Owner.InvalidateCache();
            this.OnDataSourceViewChanged(EventArgs.Empty);
            return ExtractAffectedRows(result);
        }

        protected override int ExecuteInsert(IDictionary values)
        {
            if (!this.CanInsert)
            {
                throw new NotSupportedException(ExposedSR.GetString(ExposedSR.InsertNotSupported, this.Owner.ID));
            }

            // we only change the behavior of sources that provide a DataObjectTypeName
            if (String.IsNullOrEmpty(this.DataObjectTypeName))
                return base.ExecuteInsert(values);

            Type sourceType = this.GetType(this.TypeName);
            Type dataObjectType = this.TryGetDataObjectType();

            if (values == null || values.Count == 0)
            {
                throw new InvalidOperationException(ExposedSR.GetString(ExposedSR.InsertRequiresValues, this.Owner.ID));
            }

            IDictionary insertParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
            MergeDictionaries(this.InsertParameters, values, insertParameters);
            object dataObject = this.BuildDataObject(dataObjectType, insertParameters, DataSourceOperation.Insert);

            object insertMethod = this.GetResolvedMethodData(sourceType, this.InsertMethod, dataObjectType, null, dataObject, DataSourceOperation.Insert);
            IOrderedDictionary parameters = ExtractMethodParameters(insertMethod);
            ObjectDataSourceMethodEventArgs args = new ObjectDataSourceMethodEventArgs(parameters);
            this.OnInserting(args);

            if (args.Cancel)
            {
                return 0;
            }

            object result = this.InvokeMethod(insertMethod);

            this.Owner.InvalidateCache();
            this.OnDataSourceViewChanged(EventArgs.Empty);
            return ExtractAffectedRows(result);
        }

        protected override int ExecuteUpdate(IDictionary keys, IDictionary values, IDictionary oldValues)
        {
            if (!this.CanUpdate)
            {
                throw new NotSupportedException(ExposedSR.GetString(ExposedSR.UpdateNotSupported, this.Owner.ID));
            }

            // we only change the behavior of sources that provide a DataObjectTypeName
            if (String.IsNullOrEmpty(this.DataObjectTypeName))
                return base.ExecuteUpdate(keys, values, oldValues);

            object updateMethod;
            Type sourceType = this.GetType(this.TypeName);
            Type dataObjectType = this.TryGetDataObjectType();

            if (this.ConflictDetection == ConflictOptions.CompareAllValues)
            {
                if (oldValues == null || oldValues.Count == 0)
                {
                    throw new InvalidOperationException(ExposedSR.GetString(ExposedSR.Pessimistic, ExposedSR.GetString(ExposedSR.Update), this.Owner.ID, "oldValues"));
                }

                IDictionary oldParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
                MergeDictionaries(this.UpdateParameters, oldValues, oldParameters);
                MergeDictionaries(this.UpdateParameters, keys, oldParameters);
                object oldDataObject = this.BuildDataObject(dataObjectType, oldParameters, DataSourceOperation.Update);

                IDictionary newParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
                MergeDictionaries(this.UpdateParameters, oldValues, newParameters);
                MergeDictionaries(this.UpdateParameters, keys, newParameters);
                MergeDictionaries(this.UpdateParameters, values, newParameters);
                object newDataObject = this.BuildDataObject(dataObjectType, newParameters, DataSourceOperation.Update);

                // if oldDataObject and newDataObject are the same object, this is a bit odd... but since we
                // built them old-then-new, the resulting object has the correct values. In general we aren't
                // going to be running that way.

                updateMethod = this.GetResolvedMethodData(sourceType, this.UpdateMethod, dataObjectType, oldDataObject, newDataObject, DataSourceOperation.Update);
            }
            else
            {
                IDictionary updateParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
                MergeDictionaries(this.UpdateParameters, oldValues, updateParameters);
                MergeDictionaries(this.UpdateParameters, keys, updateParameters);
                MergeDictionaries(this.UpdateParameters, values, updateParameters);
                object dataObject = this.BuildDataObject(dataObjectType, updateParameters, DataSourceOperation.Update);

                updateMethod = this.GetResolvedMethodData(sourceType, this.UpdateMethod, dataObjectType, null, dataObject, DataSourceOperation.Update);
            }

            IOrderedDictionary parameters = ExtractMethodParameters(updateMethod);
            ObjectDataSourceMethodEventArgs args = new ObjectDataSourceMethodEventArgs(parameters);
            this.OnUpdating(args);

            if (args.Cancel)
            {
                return 0;
            }

            object result = this.InvokeMethod(updateMethod);

            this.Owner.InvalidateCache();
            this.OnDataSourceViewChanged(EventArgs.Empty);
            return ExtractAffectedRows(result);
        }

        public virtual object AcquireDataObject(Type dataObjectType, IDictionary inputParameters, DataSourceOperation dataSourceOperation)
        {
            #region Generic Repository

            if (dataSourceOperation == DataSourceOperation.Insert)
            {
                return Activator.CreateInstance(dataObjectType);
            }

            Type repositoryObjectType = this.GetType(this.TypeName);

            MethodInfo methodGetPrimaryKeyValue = repositoryObjectType.GetMethod("GetPrimaryKeyValue", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
            MethodInfo methodGetEntity = repositoryObjectType.GetMethod("GetEntity", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy, null, new Type[] { typeof(object) }, null);
            PropertyInfo propertyPrimaryKey = repositoryObjectType.GetProperty("PrimaryKeyName", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);

            string primaryKeyValue = propertyPrimaryKey.GetValue(repositoryObjectType, null) as string;

            object entityPrimaryKey = inputParameters[primaryKeyValue];
            object loadedEntity = methodGetEntity.Invoke(null, new object[] { entityPrimaryKey });

            return loadedEntity;
            #endregion

            //return Activator.CreateInstance(dataObjectType);
        }

        // Oh, if only this was a virtual on ObjectDataSourceView!
        private object BuildDataObject(Type dataObjectType, IDictionary inputParameters, DataSourceOperation dataSourceOperation)
        {
            object dataObject = AcquireDataObject(dataObjectType, inputParameters, dataSourceOperation);
            PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataObjectType);

            foreach (DictionaryEntry entry in inputParameters)
            {
                string propertyName = (entry.Key == null) ? string.Empty : entry.Key.ToString();
                PropertyDescriptor descriptor = properties.Find(propertyName, true);

                if (descriptor == null)
                {
                    throw new InvalidOperationException(ExposedSR.GetString(ExposedSR.DataObjectPropertyNotFound, new object[] { propertyName, this.Owner.ID }));
                }

                if (descriptor.IsReadOnly)
                {
                    throw new InvalidOperationException(ExposedSR.GetString(ExposedSR.DataObjectPropertyReadOnly, new object[] { propertyName, this.Owner.ID }));
                }
                try
                {
                    object propertyValue = BuildObjectValue(entry.Value, descriptor.PropertyType, propertyName);
                    descriptor.SetValue(dataObject, propertyValue);
                }
                catch (Exception e)
                {
                    //catch any exceptions thrown and pass them into the Updated event if it exists
                    //if(OnUpdated != null)
                    ObjectDataSourceStatusEventArgs args = new ObjectDataSourceStatusEventArgs(null, null, e);
                    this.OnUpdated(args);
                    if (!args.ExceptionHandled)
                        throw e;
                }
            }

            return dataObject;
        }

        #region Proxies for private methods
        private GenericObjectDataSource Owner
        {
            get { return (GenericObjectDataSource)s_Owner.GetValue(this); }
        }

        private Type GetType(string typeName)
        {
            return (Type)s_GetType.Invoke(this, new object[] { typeName });
        }

        private Type TryGetDataObjectType()
        {
            return (Type)s_TryGetDataObjectType.Invoke(this, null);
        }

        private static void MergeDictionaries(ParameterCollection reference, System.Collections.IDictionary source, IDictionary destination)
        {
            s_MergeDictionaries.Invoke(null, new object[] { reference, source, destination });
        }

        private static object BuildObjectValue(object value, Type destinationType, string paramName)
        {
            return s_BuildObjectValue.Invoke(null, new object[] { value, destinationType, paramName });
        }

        private object GetResolvedMethodData(Type type, string methodName, Type dataObjectType
                                             , object oldDataObject, object newDataObject, DataSourceOperation operation)
        {
            return s_GetResolvedMethodData.Invoke(this, new object[] { type, methodName, dataObjectType, oldDataObject, newDataObject, operation });
        }

        private IOrderedDictionary ExtractMethodParameters(object method)
        {
            return (IOrderedDictionary)s_Parameters.GetValue(method);
        }

        private object InvokeMethod(object method)
        {
            return s_InvokeMethod.Invoke(this, new object[] { method });
        }

        private int ExtractAffectedRows(object result)
        {
            return (int)s_AffectedRows.GetValue(result);
        }
        #endregion
    }
}
